﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Xml;

namespace RTF2XML
{
    public class RTFParser
    {
        /// <summary>
        /// The file name of the input RTF file
        /// </summary>
        private string sFileName = "";

        /// <summary>
        /// List of the outermost groups which will contain the rest of the document.
        /// </summary>
        /// I believe this should only contain a single outer group, but I coded to allow for more just in case
        private List<GroupItem> lstGroups = new List<GroupItem>();

        /// <summary>
        /// Creates a new instance of the RTFParser that uses the input file
        /// </summary>
        /// <param name="_sFileName"></param>
        public RTFParser(string _sFileName)
        {
            //If the input file does not exist
            if (!File.Exists(_sFileName))
            {
                //Throw a new exception
                throw new FileNotFoundException("File not found", _sFileName);
            }

            this.sFileName = _sFileName;
        }

        /// <summary>
        /// Parses out the first groups that are not nested within any other groups
        /// and starts the recursive parsing of all nested groups and controls
        /// </summary>
        /// <returns>returns a list of all un-nested groups and their contents</returns>
        public List<GroupItem> ParseGroups()
        {
            //Creates a new control object.
            //This allows us to use a dictionary to get the control types and is much faster
            Control control = new Control();

            //Open the file to read
            StreamReader srReader = new StreamReader(this.sFileName);

            //Contains the outermost groups
            List<GroupItem> lstGroups = new List<GroupItem>();

            //Used to keep track of how deeply nested you are
            int iGroupNesting = 0;

            //Stack to push and pop characters as you read them
            Stack<char> stkGroup = new Stack<char>();

            //Read until you get to the end of the file
            while (!srReader.EndOfStream)
            {
                //Read in the next character
                char cCurrentChar = (char)srReader.Read();

                //A group can only start with a {
                if (cCurrentChar == '{')
                {
                    //Add the current character to the stack
                    stkGroup.Push(cCurrentChar);
                    //Increment the group nesting (you're inside a new group)
                    iGroupNesting++;
                }
                //If you're at the end of a nested group
                else if (cCurrentChar == '}' && iGroupNesting != 1)
                {
                    //Add the character to the stack
                    stkGroup.Push(cCurrentChar);
                    //Decrement the group nesting (you left a nested group)
                    iGroupNesting--;
                }
                //If you're at the end of an outer group (not nested within any others)
                else if(cCurrentChar == '}')
                {
                    //Decrement the group nesting
                    iGroupNesting--;

                    //Pop all of the characters off the stack, all this is within the group
                    string sGroup = "";
                    while (stkGroup.Count > 1)
                    {
                        char chr = stkGroup.Pop();
                        sGroup = chr + sGroup;
                    }

                    //To get rid of the beginning {
                    stkGroup.Pop();

                    //Create a new instance of a group with the text you parsed out.
                    //This also passes in an the instance of the control class to be used when parsing out control types
                    //Passes in null for the parent group since it has no parent
                    //Creating a new group will start the recursive parsing of everything within the group
                    lstGroups.Add(new Group(sGroup, control, null));
                }
                //If not at the beginning or end of a group and within a nested group
                else if (iGroupNesting != 0)
                {
                    //Add the current character to the stack
                    stkGroup.Push(cCurrentChar);
                }
                //Note that only characters within a group are considered.
                //Any text not within a group is considered invalid and not used, you may want an else statement to throw an exception here.
            }

            //Keep this list of groups
            this.lstGroups = lstGroups;
            //Return the list of groups
            return lstGroups;
        }

        /// <summary>
        /// Creates and returns an xml document for the RTF file
        /// </summary>
        /// <returns></returns>
        public XmlDocument CreateXML()
        {
            //Create a new xml document
            XmlDocument xmlDoc = new XmlDocument();
            //For each group within the list of groups
            foreach (GroupItem group in this.lstGroups)
            {
                //Recursively adds xml to the document
                group.AddXML(ref xmlDoc);
            }

            //Returns a completed xml document
            return xmlDoc;
        }

        /// <summary>
        /// Gets all the controls that weren't able to be matched and the number of times they showed up
        /// </summary>
        /// <returns>Returns a dictionary with the control names and counts</returns>
        public Dictionary<string, int> GetUnmatchedControlCount()
        {
            //A list of all the unmatched control names
            List<string> lstAllUnmatchedControls = new List<string>();
            //For each group item within the list of groups
            foreach (Group nestedGroup in this.lstGroups.Where(i => i.GetType() == typeof(Group)).ToList())
            {
                //Get a list of all the unmatched controls and concat them together
                lstAllUnmatchedControls = lstAllUnmatchedControls.Concat(nestedGroup.UnmatchedControls).ToList();
            }

            //The dictionary with the control names and counts
            Dictionary<string, int> dicUnmatchedControlCounts = new Dictionary<string, int>();
            //For each unmatched control found
            foreach (string control in lstAllUnmatchedControls)
            {
                //If the dictionary already contains the control name
                if (dicUnmatchedControlCounts.ContainsKey(control))
                {
                    //Increment that control's count
                    dicUnmatchedControlCounts[control]++;
                }
                //If the control hasn't already been added to the dictionary
                else
                {
                    //Add the control name to the dictionary and start it's count at 1
                    dicUnmatchedControlCounts.Add(control, 1);
                }
            }

            //Return the dictionary of control names and counts
            return dicUnmatchedControlCounts;
        }
    }
}
